using Commons.Utils;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq.Expressions;

namespace Commons.Injector
{
	public enum ObjectCreationState
	{
		NotCreated,
		InProgress,
		Created
	}

	public class ObjectDefinition
	{
		public string className;
		public string id;
		public bool singleton;
		public ConstructorDefinition ctorDef;
		public Dictionary<string, PropertyDefinition> properties;
		public List<SubscriptionDefinition> subscriptions;
		public ObjectCreationState state;
		public string initMethodName;
		public string factoryClassName;
		public string factoryMethodName;
		public Type[] factoryMethodGenerics;
		public string scope;

		public ObjectDefinition()
		{
			state = ObjectCreationState.NotCreated;
			properties = new Dictionary<string, PropertyDefinition>();
			subscriptions = new List<SubscriptionDefinition>();
		}

		private ConstructorInfo FindConstructor()
		{
			Type classType = ReflectionHelper.GetCustomType(className);

			if (classType == null)
			{
				Debug.LogErrorFormat("class '{0}' not found.", className);
				return null;
			}

			ConstructorInfo ctorInfo = null;
			
			if (ctorDef == null)
			{
				// default constructor.
				ctorInfo = classType.GetConstructor(Type.EmptyTypes);
			}
			else
			{
				if (ctorDef.varArgs)
				{
					Type[] types =  new Type[] { ctorDef.varType.MakeArrayType() };
					ctorInfo = classType.GetConstructor(types);
				}
				else
				{
					// constructor parametrizado.
					ctorInfo = classType.GetConstructor(ctorDef.GetConstructorTypes());
				}
			}
			if (ctorInfo == null)
			{
				Debug.LogError("constructor with supplied params does not exists.");
			}

			return ctorInfo;
		}

		private static readonly Dictionary<string, Func<object>> creatorDict = new Dictionary<string, Func<object>>();

		private object CreateObject(object[] values)
		{
			if (ctorDef == null)
			{
				Func<object> creator = null;
				creator = GetCreator(Type.EmptyTypes);
				return creator();
			}

			Type classType = ReflectionHelper.GetCustomType(className);
			ConstructorInfo ctorInfo = null;

			if (ctorDef.varArgs)
			{
				Type[] types =  new Type[] { ctorDef.varType.MakeArrayType() };
				ctorInfo = classType.GetConstructor(types);
			}
			else
			{
				// constructor parametrizado.
				ctorInfo = classType.GetConstructor(ctorDef.GetConstructorTypes());
			}

			if (ctorInfo != null)
			{
				return ctorInfo.Invoke(values);
			}

			Debug.LogError("constructor with supplied params does not exists.");
			return null;
		}

		private Func<object> GetCreator(Type[] args)
		{
			Func<object> creator;

			if (! creatorDict.TryGetValue(className, out creator))
			{
				Type classType = ReflectionHelper.GetCustomType(className);
				if (classType == null)
				{
					Debug.LogErrorFormat("class '{0}' not found.", className);
					return null;
				}
				creator = delegate () { return classType.GetConstructor(args).Invoke(null); };
				creatorDict[className] = creator;
			}

			return creator;
		}

		private MethodInfo FindFactoryMethod()
		{
			Type classType = ReflectionHelper.GetCustomType(factoryClassName);

			if (classType == null)
			{
				Debug.LogWarning("factory-class \"" + factoryClassName + "\" not found.");
				return null;
			}

			MethodInfo methodInfo = classType.GetMethod(factoryMethodName, ctorDef == null ? Type.EmptyTypes : ctorDef.GetConstructorTypes());
			if (factoryMethodGenerics.Length > 0)
			{
				return methodInfo.MakeGenericMethod(factoryMethodGenerics);
			}
			else
			{
				return methodInfo;
			}
		}

		public virtual object Instantiate(Dictionary<string,object> globalReferences, Dictionary<string,object> localReferences)
		{
			object[] values = null;
			if (ctorDef != null)
			{
				values = globalReferences == null ? ctorDef.GetConstructorValues() : ctorDef.GetConstructorValues(globalReferences, localReferences);
			}

			if (factoryMethodName == null)
			{
				return CreateObject(values);
			}
			else
			{
				MethodInfo factoryInfo = FindFactoryMethod();
				try
				{
					return factoryInfo.Invoke(null, values);
				}
				catch(TargetException ex)
				{
					Debug.LogErrorFormat("Error while calling factory of '{0}': {1}", id, ex.Message);
					return null;
				}
			}
		}
	}
}
